import os
import signal
import logging
import secrets
from urllib.parse import urlparse, urlunparse

from werkzeug import LocalProxy
from flask import (
    Flask, Blueprint,
    request, current_app, jsonify, stream_with_context,
)
from flask_basicauth import BasicAuth
from jinja2 import Template

from tinyops.library.helper import generate_token, verify_token
from tinyops.library.json_helper import TinyopsJSONEncoder
from tinyops.config import Config
from tinyops.task import TaskQueue
from tinyops.alerter import AlerterManager


LOG = logging.getLogger(__name__)

bp = Blueprint('api', __name__)
bp_webhook = Blueprint('webhook', __name__)


def current_config():
    return current_app.config['TINYOPS_CONFIG']


CONFIG: Config = LocalProxy(current_config)


def stream_response(generator):
    gen = stream_with_context(generator)()
    return current_app.response_class(
        gen, mimetype='text/event-stream',
        headers=[('Cache-Control', 'no-cache'), ('X-Accel-Buffering', 'no')],
    )


INDEX_TEMPLATE_CONTENT = """
TinyOps
=======

GET  /                          Document
GET  /config                    Get config in JSON format
POST /config/reload             Reload config
POST /task/<name>               Run task
GET  /webhook/<name>            Get webhook token
POST /webhook/<name>/<token>    Run task by webhook

{% for task in tasks -%}
Task: {{ task.name }}
curl -i -X POST {{ url_prefix }}/task/{{ task.name }}
{% endfor -%}

{% for webhook in webhooks %}
Webhook: {{ webhook.name }} task={{ webhook.task }}
curl -i -X POST {{ webhook_url_prefix }}/webhook/{{ webhook.name }}/{{ webhook.token }}
{% endfor -%}

{% for timer in timers %}
Timer: {{ timer.name }} task={{ timer.task }} timer={{ timer.timer }}
{% endfor %}
"""

INDEX_TEMPLATE = Template(INDEX_TEMPLATE_CONTENT)


@bp.route('/')
def index():
    tasks = {}
    for x in CONFIG.tasks:
        tasks[x.name] = dict(name=x.name)
    webhooks = {}
    for x in CONFIG.webhooks:
        token = generate_token(x.secret, value=x.name)
        webhooks[x.name] = dict(name=x.name, task=x.task, token=token)
    timers = {}
    for x in CONFIG.timers:
        timers[x.name] = dict(name=x.name, task=x.task, timer=x.timer)
    tasks = list(sorted(tasks.values(), key=lambda x: x['name']))
    webhooks = list(sorted(webhooks.values(), key=lambda x: x['name']))
    timers = list(sorted(timers.values(), key=lambda x: x['name']))
    url_prefix = request.base_url.rstrip('/')
    webhook_url_prefix = CONFIG.webui.webhook_url_prefix
    if not webhook_url_prefix:
        webhook_port = CONFIG.webui.webhook_port
        p = urlparse(url_prefix)
        webhook_url_prefix = urlunparse((
            p.scheme, f'{p.hostname}:{webhook_port}', p.path, None, None, None))
    return INDEX_TEMPLATE.render(
        tasks=tasks,
        webhooks=webhooks,
        timers=timers,
        url_prefix=url_prefix,
        webhook_url_prefix=webhook_url_prefix,
    )


@bp.route('/config')
def config_get():
    return jsonify(CONFIG.raw_secure)


@bp.route('/config/reload', methods=['POST'])
def config_reload():
    reloader = current_app.config['TINYOPS_RELOADER']
    if not reloader:
        return jsonify({"message": "not support reload config"}), 400
    pid, sig = reloader
    sig_name = signal.Signals(sig).name
    LOG.info(f"reload config, signal {sig_name} to pid {pid}")
    os.kill(pid, sig)
    return jsonify({"message": "reload config... wait 3 seconds"})


@bp.route('/task/<task_name>', methods=['GET', 'POST'])
def task_run(task_name):
    try:
        with TaskQueue(CONFIG) as queue:
            queue.submit(task_name)
    except ValueError as ex:
        return jsonify({'message': str(ex)}), 400
    return jsonify({'message': f'task {task_name} submited'})


@bp.route('/webhook/<name>', methods=['GET'])
def webhook_token(name):
    webhook = None
    for x in CONFIG.webhooks:
        if name == x.name:
            webhook = x
            break
    if not webhook:
        return jsonify({'message': f'webhook {name} not found'}), 400
    token = generate_token(webhook.secret, value=webhook.name)
    return jsonify({
        'webhook': name,
        'token': token,
        'task': webhook.task,
    })


@bp_webhook.route('/webhook/<name>/<token>', methods=['GET', 'POST'])
def webhook_run(name, token):
    webhooks = {x.name: x for x in CONFIG.webhooks}
    if name not in webhooks:
        return jsonify({'message': f'webhook {name} not exists'}), 400
    webhook = webhooks[name]
    if not verify_token(webhook.secret, name, token):
        return jsonify({'message': f'webhook {name} token invalid'}), 403
    try:
        with TaskQueue(CONFIG) as queue:
            queue.submit(webhook.task)
    except ValueError as ex:
        return jsonify({'message': str(ex)}), 400
    return jsonify({'message': f'task {webhook.task} submited'})


@bp_webhook.route('/alert/<secret>/<title>', methods=['GET', 'POST'])
def alert_send(secret, title):
    if not CONFIG.alerter:
        return jsonify({'message': 'alerter not configured'}), 404
    is_verified = secrets.compare_digest(CONFIG.alerter.secret, secret)
    if not is_verified:
        return jsonify({'message': 'alerter secret invalid'}), 403
    body = request.get_data().decode('utf-8')
    alerter_manager = AlerterManager(CONFIG)
    alerter_manager.send(title=title, body=body)
    return jsonify({'message': f'alert {title} send'})


def create_app(config, only_webhook=False, reloader=None):
    app = Flask(__name__)
    app.json_encoder = TinyopsJSONEncoder
    app.config['JSON_AS_ASCII'] = False
    subpath = config.webui.subpath.rstrip().rstrip('/')
    if not only_webhook:
        app.register_blueprint(bp, url_prefix=subpath)
    app.register_blueprint(bp_webhook, url_prefix=subpath)
    app.debug = config.debug
    app.config['DEBUG'] = config.debug
    basic_auth_config = config.webui.basic_auth
    if basic_auth_config:
        app.config['BASIC_AUTH_USERNAME'] = basic_auth_config['username']
        app.config['BASIC_AUTH_PASSWORD'] = basic_auth_config['password']
        auth = BasicAuth(app)

        @bp.before_request
        def check_auth():
            if not auth.authenticate():
                return auth.challenge()

        # Note: bp_webhook not require basic auth

    app.config['TINYOPS_CONFIG'] = config
    app.config['TINYOPS_RELOADER'] = reloader
    return app
